use actix_web::{HttpResponse, Responder, get, web};
use clap::Parser;
use config::{Config, ConfigBuilder, builder::DefaultState};
use if_addrs::IfAddr;
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use serde::{Deserialize, Serialize};
use std::time::Duration;

use crate::consul::{Check, Service};

pub mod consul;
pub mod controller;
pub mod entity;
pub mod model;
pub mod service;

#[derive(Debug, Parser)]
#[command()]
struct Cli {
    #[arg(
        long = "spring.config.location",
        env = "SPRING_CONFIG_LOCATION",
        default_value = "./conf/application.yml"
    )]
    config_location: String,
    #[arg(long = "spring.profiles.active", env = "SPRING_PROFILES_ACTIVE")]
    profiles_active: Option<String>,
}

#[derive(Clone, Debug, Default)]
pub struct Configuration {
    pub config: Config,
}

impl Configuration {
    pub fn new() -> Self {
        let app_config = Configuration::local_config_builder().build().unwrap();

        Configuration { config: app_config }
    }

    pub async fn from_consul() -> Self {
        let local_config = Configuration::new();
        let application_name = local_config.application_name();
        let profiles_active = local_config.profiles_active();
        let server_port = local_config.server_port();
        let consul_host = local_config.consul_host();
        let consul_port = local_config.consul_port();
        let consul_config_prefix = local_config.consul_config_prefix();

        let ip = local_ip();
        let config_key = format!(
            "{}/{},{}/data",
            consul_config_prefix, application_name, profiles_active
        );

        let consul_client = consul::Client::new(consul_host, consul_port);

        let id = format!("{}-{}-{}", application_name, ip, server_port);
        let check_http = format!("http://{}:{}/actuator/health", ip, server_port);

        let _ = consul_client
            .agent()
            .register_service(Service {
                id: id,
                name: application_name,
                address: ip,
                port: server_port,
                check: Some(Check {
                    http: check_http,
                    interval: "3s".to_string(),
                    timeout: "60s".to_string(),
                    deregister_critical_service_after: "2s".to_string(),
                }),
                ..Default::default()
            })
            .await
            .unwrap();

        let kv_pair = consul_client
            .kv()
            .read_key(config_key.as_str())
            .await
            .unwrap();
        let config_value = kv_pair.value_text();

        let config_builder = Configuration::local_config_builder().add_source(
            config::File::from_str(config_value.as_str(), config::FileFormat::Yaml),
        );

        let app_config = config_builder.build().unwrap();

        Configuration { config: app_config }
    }

    pub fn application_name(&self) -> String {
        self.config
            .get_string("spring.application.name")
            .unwrap_or("ross".to_string())
    }

    pub fn profiles_active(&self) -> String {
        self.config
            .get_string("spring.profiles.active")
            .unwrap_or("dev".to_string())
    }

    pub fn server_address(&self) -> String {
        self.config
            .get_string("server.address")
            .unwrap_or("0.0.0.0".to_string())
    }

    pub fn server_port(&self) -> u16 {
        self.config.get_int("server.port").unwrap_or(9000) as u16
    }

    pub fn logging_level(&self) -> String {
        self.config
            .get_string("logging.level")
            .unwrap_or("info".to_string())
    }

    pub async fn database_connection(
        &self,
    ) -> std::result::Result<DatabaseConnection, Box<dyn std::error::Error>> {
        let url = self.config.get_string("spring.datasource.url")?;
        let max_connections = self
            .config
            .get_int("spring.datasource.max-connections")
            .unwrap_or(100) as u32;
        let min_connections = self
            .config
            .get_int("spring.datasource.min-connections")
            .unwrap_or(1) as u32;
        let connect_timeout = self
            .config
            .get_int("spring.datasource.connect_timeout")
            .unwrap_or(30) as u64;
        let acquire_timeout = self
            .config
            .get_int("spring.datasource.acquire-timeout")
            .unwrap_or(8) as u64;
        let idle_timeout = self
            .config
            .get_int("spring.datasource.idle-time")
            .unwrap_or(10) as u64;
        let max_lifetime = self
            .config
            .get_int("spring.datasource.max-lifetime")
            .unwrap_or(30) as u64;

        let mut opt = ConnectOptions::new(url);

        opt.max_connections(max_connections)
            .min_connections(min_connections)
            .connect_timeout(Duration::from_secs(connect_timeout))
            .acquire_timeout(Duration::from_secs(acquire_timeout))
            .idle_timeout(Duration::from_secs(idle_timeout))
            .max_lifetime(Duration::from_secs(max_lifetime));

        let database_connection: DatabaseConnection = Database::connect(opt).await?;

        Ok(database_connection)
    }

    pub fn consul_host(&self) -> String {
        self.config
            .get_string("spring.cloud.consul.host")
            .unwrap_or("127.0.0.1".to_string())
    }

    pub fn consul_port(&self) -> u16 {
        self.config
            .get_int("spring.cloud.consul.host")
            .unwrap_or(8500) as u16
    }

    pub fn consul_config_prefix(&self) -> String {
        self.config
            .get_string("spring.cloud.consul.config.prefixes")
            .unwrap_or_default()
    }

    pub fn oss_local_storage_path(&self) -> String {
        self.config
            .get_string("oss.local-storage.path")
            .unwrap_or_default()
    }

    fn local_config_builder() -> ConfigBuilder<DefaultState> {
        let args = Cli::parse();
        let mut config_builder =
            Config::builder().add_source(config::File::with_name(&args.config_location));

        if let Some(v) = args.profiles_active {
            config_builder = config_builder
                .set_override("spring.profiles.active", v)
                .unwrap();
        }

        config_builder
    }
}

#[derive(Clone, Debug, Default)]
pub struct AppState {
    pub configuration: Configuration,
    pub database_connection: DatabaseConnection,
}

pub fn local_ip() -> String {
    if_addrs::get_if_addrs()
        .ok()
        .unwrap()
        .into_iter()
        .find(|iface| !iface.is_loopback() && matches!(iface.addr, IfAddr::V4(_)))
        .and_then(|iface| match iface.addr {
            IfAddr::V4(addr) => Some(addr.ip.to_string()),
            _ => Some("127.0.0.1".to_string()),
        })
        .unwrap()
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HealthCheckResult {
    status: String,
}

impl HealthCheckResult {
    pub fn new() -> Self {
        Self {
            status: "UP".to_string(),
        }
    }
}

#[get("/health")]
pub async fn health() -> impl Responder {
    HttpResponse::Ok().json(HealthCheckResult::new())
}

pub fn routers(cfg: &mut web::ServiceConfig) {
    cfg.service(web::scope("/actuator").service(health));

    controller::router::routers(cfg);
}
